package cn.coder.toolkit;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * 在高并发场景下, 一定要确保使用同一个 OkHttpClient 对象, 否则容易造成内存溢出
 * 因为每个 OkHttpClient 对象都有一个 connectionPool 属性, 其中的连接在使用后默认会继续存活5分钟, 即该对象只有到期后才会被释放
 * 而且每个池中有一个线程池对象, 会创建一个清理连接的死循环线程, 当有大量连接池对象时, 也会有大量该线程, 造成无谓的资源浪费
 * 在高并发场景下, 如果每次请求都创建一个新的 OkHttpClient 对象(及 ConnectionPool), 这些对象会随着时间推移(默认5分钟内)占用越来越多的内存, 最终导致内存爆炸
 * 参考: <a href="https://blog.csdn.net/mrathena/article/details/123267897">...</a>
 * <p>
 * 不正确的封装与使用可能会报如下内存泄漏异常, 原因是 Response / ResponseBody 没有正确且及时关闭, 响应关闭应封装到公共方法中而不能靠方法调用者的自觉关闭(非常不靠谱)
 * WARN okhttp3.OkHttpClient - A connection to <a href="http://host:port">...</a> was leaked. Did you forget to close a response body? To see where this was allocated, set the OkHttpClient logger level to FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
 * <p>
 * 使用异步 enqueue 时, 最终会调用到 OkHttpClient 内 Dispatcher 对象内部维护的一个线程池, 该线程池中的线程默认有60秒的存活时间(导致执行结束后不能及时停止), 这里可更换自定义的线程池. 不使用异步 enqueue 没有该问题
 * new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
 * new OkHttpClient.Builder().dispatcher(new Dispatcher(executor)).build();
 * <p>
 * 通过 response.body().byteStream() 可以拿到 InputStream, 可依此实现下载功能, buffer=8196, 可依此实现下载进度功能
 */
@Slf4j
public final class HttpKit {

    public static void main(String[] args) {
        System.out.println(HttpKit.get().url("https://www.baidu.com").execute());
    }

    private HttpKit() {}

    public static final MediaType MEDIA_TYPE_APPLICATION_JSON_UTF8 = MediaType.get("application/json;charset=utf-8");
    public static final MediaType MEDIA_TYPE_TEXT_PLAIN_UTF8 = MediaType.get("text/plain; charset=utf-8");
    public static final MediaType MEDIA_TYPE_APPLICATION_OCTET_STREAM = MediaType.get("application/octet-stream");

    private static final OkHttpClient CLIENT = getClient(60, 60, 60);

    public static OkHttpClient getClient(int connect, int write, int read) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        // timeout
        builder.connectTimeout(connect, TimeUnit.SECONDS);
        builder.writeTimeout(write, TimeUnit.SECONDS);
        builder.readTimeout(read, TimeUnit.SECONDS);
        // cookie
        builder.setCookieJar$okhttp(new CookieJar() {

            // https://www.rfc-editor.org/rfc/rfc6265#section-5.1.3, Domain Matching
            // https://www.rfc-editor.org/rfc/rfc6265#section-5.3, 如何处理 Cookie
            // https://www.rfc-editor.org/rfc/rfc6265#section-5.3, 6. 如何处理 Cookie 的 Domain
            // 结构应该是 Map<Domain, Map<CookieName, CookieValue>>, 而不是用 host 做 key
            private final Map<String, Map<String, Cookie>> cookies = new ConcurrentHashMap<>();

            @Override
            public void saveFromResponse(HttpUrl url, List<Cookie> list) {
                // 入参的 cookies 里存在多个 cookie 有相同 name, 不同 domain 的情况, 所以不能直接弄转成 map
                for (Cookie cookie : list) {
                    if (!cookie.value().isEmpty() && cookie.matches(url)) {
                        String domain = cookie.domain();
                        this.cookies.putIfAbsent(domain, new ConcurrentHashMap<>());
                        this.cookies.get(domain).put(cookie.name(), cookie);
                    }
                }
                this.cookies.forEach((domain, value) -> {
                    value.forEach((name, cookie) -> log.debug("Cookie Save: [{}], [{}={}]", domain, name, cookie.value()));
                });
            }

            @Override
            public List<Cookie> loadForRequest(HttpUrl url) {
                List<Cookie> cookieList = new LinkedList<>();
                this.cookies.forEach((domain, map) -> {
                    for (Cookie cookie : map.values()) {
                        // 这里的匹配逻辑不是我们自己瞎比猜的, 看上面的参考文档, 另外 OkHttp3.Cookie 已经按国际标准实现了 cookie 是否匹配 url 的验证方法
                        if (cookie.matches(url)) {
                            cookieList.add(cookie);
                            log.debug("Cookie Load: [{}], [{} = {}]", domain, cookie.name(), cookie.value());
                        }
                    }
                });
                return cookieList;
            }
        });
        // 绕过 Https 相关的证书域名验证
        try {
            X509TrustManager manager = new DefaultX509TrustManager();
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{manager}, new java.security.SecureRandom());
            builder.sslSocketFactory(sslContext.getSocketFactory(), manager);
            builder.hostnameVerifier((hostname, session) -> true);
        } catch (KeyManagementException | NoSuchAlgorithmException e) {
            log.error("", e);
        }
        // 解决异步调用需要过1分钟才能结束的问题
//        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 5, TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
//        builder.dispatcher(new Dispatcher(executor));
        return builder.build();
    }

    private static class DefaultX509TrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {}
        @Override
        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {}
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    }

    public static Map<String, String> getCookies(OkHttpClient client, String url) {
        List<Cookie> cookies = client.cookieJar().loadForRequest(HttpUrl.get(url));
        if (cookies.isEmpty()) {
            return null;
        }
        Map<String, String> result = new HashMap<>();
        for (Cookie cookie : cookies) {
            result.put(cookie.name(), cookie.value());
        }
        return result;
    }

    public static String getCookie(OkHttpClient client, String url, String cookieName) {
        Map<String, String> cookies = getCookies(client, url);
        if (null == cookies) {
            return null;
        }
        return cookies.get(cookieName);
    }

    public static GetBuilder get(OkHttpClient client) {
        return new Get(client);
    }

    public static PostBuilder post(OkHttpClient client) {
        return new Post(client);
    }

    public static GetBuilder get() {
        return new Get(HttpKit.CLIENT);
    }

    public static PostBuilder post() {
        return new Post(HttpKit.CLIENT);
    }

    public interface HttpBuilder<T extends HttpBuilder<T>> {
        <E> E execute(Function<Response, E> function);
        ResponseWrapper execute();
        String string();
        void enqueue(Consumer<Response> consumer);
        void string(Consumer<String> consumer);
    }

    public interface RequestBuilder<T extends RequestBuilder<T>> extends HttpBuilder<T> {
        T url(String url);
        T header(String key, String value);
        T headers(Map<String, String> headers);
        T cookie(String value);
        T parameter(String key, String value);
        T parameters(Map<String, Object> parameters);
    }

    public interface GetBuilder extends RequestBuilder<GetBuilder> {}

    public interface PostBuilder extends RequestBuilder<PostBuilder> {
        PostBuilder withMultipart(); // 有且仅有 Parameters 时生效, 使用 multipart/form-data, 否则使用 application/x-www-form-urlencoded
        PostBuilder json(String json);
        PostBuilder json(String json, MediaType mediaType);
        PostBuilder json(String json, String mediaType);
        PostBuilder text(String text);
        PostBuilder text(String text, MediaType mediaType);
        PostBuilder text(String text, String mediaType);
        PostBuilder file(String key, Path path);
        PostBuilder files(String key, List<Path> paths);
        PostBuilder files(Map<String, Path> paths);
        PostBuilder fileS(Map<String, List<Path>> paths);
        PostBuilder empty(MediaType mediaType);
    }

    public static class Get implements GetBuilder {
        private final OkHttpClient client;
        private String url;
        private final Map<String, String> headers = new HashMap<>();
        private final Map<String, Object> parameters = new HashMap<>();

        public Get(OkHttpClient client) {
            this.client = client;
        }

        @Override
        public GetBuilder url(String url) {
            this.url = url;
            return this;
        }

        @Override
        public GetBuilder header(String key, String value) {
            this.headers.put(key, value);
            return this;
        }

        @Override
        public GetBuilder headers(Map<String, String> headers) {
            this.headers.putAll(headers);
            return this;
        }

        @Override
        public GetBuilder cookie(String value) {
            return header("Cookie", value);
        }

        @Override
        public GetBuilder parameter(String key, String value) {
            this.parameters.put(key, value);
            return this;
        }

        @Override
        public GetBuilder parameters(Map<String, Object> parameters) {
            this.parameters.putAll(parameters);
            return this;
        }

        @Override
        public <T> T execute(Function<Response, T> function) {
            Request request = Helper.buildRequest(this);
            return Helper.execute(this.client, request, function);
        }

        @Override
        public ResponseWrapper execute() {
            return execute(Helper::getResponseWrapper);
        }

        @Override
        public String string() {
            return execute(Helper::getString);
        }

        @Override
        public void enqueue(Consumer<Response> callback) {
            Request request = Helper.buildRequest(this);
            Helper.enqueue(this.client, request, callback);
        }

        @Override
        public void string(Consumer<String> consumer) {
            enqueue(response -> Helper.consumeResponse(consumer, response));
        }
    }

    public static class Post implements PostBuilder {
        private final OkHttpClient client;
        private String url;
        private final Map<String, String> headers = new HashMap<>();
        private final Map<String, Object> parameters = new HashMap<>();
        private Boolean encodeParametersWithMultipartFormData; // 默认使用 application/x-www-form-urlencoded, 除非指定使用 multipart/form-data
        private String json; // application/json
        private MediaType jsonMediaType; // 不存在则走默认 MediaType.get("application/json; charset=utf-8")
        private String text; // text/plain
        private MediaType textMediaType; // 不存在则走默认 MediaType.get("text/plain; charset=utf-8")
        private final Map<String, List<Path>> paths = new HashMap<>(); // application/octet-stream
        private MediaType emptyMediaType; // 如果 parameters/paths/json/text 都没有值时, 走这个 MediaType

        public Post(OkHttpClient client) {
            this.client = client;
        }

        @Override
        public PostBuilder url(String url) {
            this.url = url;
            return this;
        }

        @Override
        public PostBuilder header(String key, String value) {
            this.headers.put(key, value);
            return this;
        }

        @Override
        public PostBuilder headers(Map<String, String> headers) {
            this.headers.putAll(headers);
            return this;
        }

        @Override
        public PostBuilder cookie(String value) {
            return header("Cookie", value);
        }

        @Override
        public PostBuilder parameter(String key, String value) {
            this.parameters.put(key, value);
            return this;
        }

        @Override
        public PostBuilder parameters(Map<String, Object> parameters) {
            this.parameters.putAll(parameters);
            return this;
        }

        @Override
        public PostBuilder withMultipart() {
            this.encodeParametersWithMultipartFormData = true;
            return this;
        }

        @Override
        public PostBuilder json(String json) {
            this.json = json;
            return this;
        }

        @Override
        public PostBuilder json(String json, MediaType mediaType) {
            this.json = json;
            this.jsonMediaType = mediaType;
            return this;
        }

        @Override
        public PostBuilder json(String json, String mediaType) {
            this.json = json;
            this.jsonMediaType = MediaType.get(mediaType);
            return this;
        }

        @Override
        public PostBuilder text(String text) {
            this.text = text;
            return this;
        }

        @Override
        public PostBuilder text(String text, MediaType mediaType) {
            this.text = text;
            this.textMediaType = mediaType;
            return this;
        }

        @Override
        public PostBuilder text(String text, String mediaType) {
            this.text = text;
            this.textMediaType = MediaType.get(mediaType);
            return this;
        }

        @Override
        public PostBuilder file(String key, Path path) {
            this.paths.computeIfAbsent(key, k -> new LinkedList<>()).add(path);
            return this;
        }

        @Override
        public PostBuilder files(String key, List<Path> paths) {
            this.paths.computeIfAbsent(key, k -> new LinkedList<>()).addAll(paths);
            return this;
        }

        @Override
        public PostBuilder files(Map<String, Path> paths) {
            for (Map.Entry<String, Path> entry : paths.entrySet()) {
                this.file(entry.getKey(), entry.getValue());
            }
            return this;
        }

        @Override
        public PostBuilder fileS(Map<String, List<Path>> paths) {
            for (Map.Entry<String, List<Path>> entry : paths.entrySet()) {
                this.files(entry.getKey(), entry.getValue());
            }
            return this;
        }

        @Override
        public PostBuilder empty(MediaType mediaType) {
            this.emptyMediaType = mediaType;
            return this;
        }

        @Override
        public <T> T execute(Function<Response, T> function) {
            Request request = Helper.buildRequest(this);
            return Helper.execute(this.client, request, function);
        }

        @Override
        public ResponseWrapper execute() {
            return execute(Helper::getResponseWrapper);
        }

        @Override
        public String string() {
            return execute(Helper::getString);
        }

        @Override
        public void enqueue(Consumer<Response> callback) {
            Request request = Helper.buildRequest(this);
            Helper.enqueue(this.client, request, callback);
        }

        @Override
        public void string(Consumer<String> consumer) {
            enqueue(response -> Helper.consumeResponse(consumer, response));
        }
    }

    private static class Helper {

        public static Request buildRequest(Get instance) {
            HttpUrl.Builder urlBuilder = Objects.requireNonNull(HttpUrl.parse(instance.url)).newBuilder();
            instance.parameters.forEach((key, value) -> {
                urlBuilder.addQueryParameter(key, value.toString());
            });
            Request.Builder builder = new Request.Builder().url(urlBuilder.build());
            instance.headers.forEach(builder::addHeader);
            return builder.build();
        }

        public static Request buildRequest(Post instance) {
            Request.Builder builder = new Request.Builder().url(instance.url);
            instance.headers.forEach(builder::addHeader);
            if (null != instance.json) {
                builder.post(RequestBody.create(instance.json, null == instance.jsonMediaType ? MEDIA_TYPE_APPLICATION_JSON_UTF8 : instance.jsonMediaType));
            } else if (null != instance.text) {
                builder.post(RequestBody.create(instance.text, null == instance.textMediaType ? MEDIA_TYPE_TEXT_PLAIN_UTF8 : instance.textMediaType));
            } else {
                // parameters 和 files 的四种情况
                if (instance.parameters.isEmpty() && instance.paths.isEmpty()) {
                    // 都没有
                    builder.post(RequestBody.create(new byte[0], null == instance.emptyMediaType ? MEDIA_TYPE_APPLICATION_JSON_UTF8 : instance.emptyMediaType));
                } else if (instance.parameters.isEmpty()) {
                    // 只有文件, multipart/form-data
                    MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
                    instance.paths.forEach((name, paths) -> {
                        for (Path path : paths) {
                            multipartBodyBuilder.addFormDataPart(name, path.getFileName().toString(), RequestBody.create(path.toFile(), MEDIA_TYPE_APPLICATION_OCTET_STREAM));
                        }
                    });
                    builder.post(multipartBodyBuilder.build());
                } else if (instance.paths.isEmpty()) {
                    // 只有参数
                    if (null != instance.encodeParametersWithMultipartFormData && instance.encodeParametersWithMultipartFormData) {
                        // multipart/form-data
                        MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
                        instance.parameters.forEach((name, value) -> {
                            multipartBodyBuilder.addFormDataPart(name, String.valueOf(value));
                        });
                        builder.post(multipartBodyBuilder.build());
                    } else {
                        // application/x-www-form-urlencoded
                        FormBody.Builder formBodyBuilder = new FormBody.Builder();
                        instance.parameters.forEach((key, value) -> formBodyBuilder.add(key, String.valueOf(value)));
                        builder.post(formBodyBuilder.build());
                    }
                } else {
                    // 都有, multipart/form-data
                    MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
                    instance.parameters.forEach((name, value) -> {
                        multipartBodyBuilder.addFormDataPart(name, String.valueOf(value));
                    });
                    instance.paths.forEach((name, paths) -> {
                        for (Path path : paths) {
                            multipartBodyBuilder.addFormDataPart(name, path.getFileName().toString(), RequestBody.create(path.toFile(), MEDIA_TYPE_APPLICATION_OCTET_STREAM));
                        }
                    });
                    builder.post(multipartBodyBuilder.build());
                }
            }
            return builder.build();
        }

        public static <T> T execute(OkHttpClient client, Request request, Function<Response, T> function) {
            // 自带 Response.close
            try (Response response = client.newCall(request).execute()) {
                return null == function ? null : function.apply(response);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        public static void enqueue(OkHttpClient client, Request request, Consumer<Response> consumer) {
            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    throw new RuntimeException(e);
                }

                @Override
                public void onResponse(Call call, Response response) {
                    try {
                        if (null != consumer) {
                            consumer.accept(response);
                        }
                    } finally {
                        response.close();
                    }
                }
            });
        }

        private static ResponseWrapper getResponseWrapper(Response response) {
            ResponseWrapper responseWrapper = new ResponseWrapper();
            responseWrapper.success = response.isSuccessful();
            responseWrapper.failure = !response.isSuccessful();
            responseWrapper.code = response.code();
            responseWrapper.message = response.message();
            try {
                if (null != response.body()) {
                    // 这种写法不一定合适, 因为有些请求返回的不是字符串, 强转成字符串只是为了适应大多数返回 Json 的场景, 其他场景应该使用 Function 参数的那个 execute 方法
                    responseWrapper.body = response.body().string();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return responseWrapper;
        }

        private static String getString(Response response) {
            if (response.isSuccessful()) {
                ResponseBody body = response.body();
                try {
                    return null == body ? null : body.string();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            } else {
                String message = response.message();
                if (message.isEmpty()) {
                    message = String.format("%d, %s", response.code(), response.request().url());
                } else {
                    message = String.format("%d:%s, %s", response.code(), message, response.request().url());
                }
                throw new RuntimeException(message);
            }
        }

        private static void consumeResponse(Consumer<String> consumer, Response response) {
            if (response.isSuccessful()) {
                ResponseBody body = response.body();
                if (null != body) {
                    try {
                        consumer.accept(body.string());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            } else {
                String message = response.message();
                if (message.isEmpty()) {
                    message = String.format("%d, %s", response.code(), response.request().url());
                } else {
                    message = String.format("%d:%s, %s", response.code(), message, response.request().url());
                }
                throw new RuntimeException(message);
            }
        }

    }

    @Data
    public static class ResponseWrapper {
        private Boolean success; // response.isSuccessful(), 200-299
        private Boolean failure; // !response.isSuccessful()
        private Integer code; // 状态码
        private String message; //
        private String body; // 成功时的响应体字符串
    }

}
